#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include <fcntl.h>
#include <utime.h>
#include <errno.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>

/* copied from "libbb.h" */
enum {	/* DO NOT CHANGE THESE VALUES!  cp.c depends on them. */
	FILEUTILS_PRESERVE_STATUS = 1,
	FILEUTILS_DEREFERENCE = 2,
	FILEUTILS_RECUR = 4,
	FILEUTILS_FORCE = 8,
	FILEUTILS_INTERACTIVE = 0x10,
	FILEUTILS_MAKE_HARDLINK = 0x20,
	FILEUTILS_MAKE_SOFTLINK = 0x40,
};

#ifdef __SWITCHROOT_MAIN__
#include <stdio.h>
#define PRINTF printf

#else
#define PRINTF(fmt, args...)                    \
    do {                                        \
        if (_msgf)                              \
            _msgf(_msgdev, fmt, ##args);        \
    } while (0)
#endif

typedef void (*MSGF_TYPE)(int, const char *, ...);
static MSGF_TYPE _msgf = NULL;
static int _msgdev = 0;

/* functions from busybox */
extern int copy_file(const char *source, const char *dest, int flags);
extern int remove_file(const char *path, int flags);
extern char *concat_subpath_file(const char *path, const char *f);
extern char *xmalloc_readlink_or_warn(const char *path);

static int filter_run(const char *dir_name,
                      int (*func)(char*, char*, __mode_t)) {
    char *skip_dirs[] = {"dev", "proc", "sys", "tmp", "etc", "mnt", NULL};
    int skip;
    char *src;
    int i;
    int result = 0;

    struct stat st;
    DIR *dp;
    struct dirent *d;

    if ((dp = opendir(dir_name)) == NULL) {
        PRINTF("unable to open directory `%s'\n", dir_name);
        return -1;
    }

    while ((d = readdir(dp)) != NULL &&
           result == 0) {
        skip = 0;
        for (i = 0; skip_dirs[i]; i++) {
            if (!strcmp(d->d_name, skip_dirs[i])) {
                skip = 1;
                break;
            }
        }

        if (!skip) {
            src = concat_subpath_file(dir_name, d->d_name);
            /* skip '.' & '..' */
            if (src == NULL)
                continue;

            if (lstat(src, &st) < 0) {
                PRINTF("Cannot stat %s\n", src);
                result = -1;
                goto err0;
            }
            /* invoke callback */
            result = func(d->d_name, src, st.st_mode);

          err0:
            free(src);
        }
    }
    closedir(dp);
    return result;
}

static int clean_root(char *d_name, char *src, __mode_t st_mode)
{
#ifdef __SWITCHROOT_MAIN__
    PRINTF("rm -rf %s\n", src);
#else
    if (remove_file(src, FILEUTILS_RECUR | FILEUTILS_FORCE)) {
        PRINTF("Cannot remove %s.\n", src);
        return -1;
    }
#endif
    return 0;
}

static int bind_root(char *d_name, char *src, __mode_t st_mode)
{
    int result = 0;
    char *dest;

    dest= concat_subpath_file("/", d_name);

    if (S_ISDIR(st_mode)) {
#ifdef __SWITCHROOT_MAIN__
        PRINTF("bind %s to %s\n", src, dest);
#else
        if (mkdir(dest, S_IRWXU) ||
            mount(src, dest, NULL, MS_BIND, NULL)) {
            PRINTF("cannot bind `%s' with mount.\n", src);
            result = -1;
            goto err0;
        }
#endif
    }
    else if (S_ISLNK(st_mode)) {
        char *lpath;

        lpath = xmalloc_readlink_or_warn(src);
        if (lpath == NULL) {
            PRINTF("readlink(%s) failed\n", src);
            result = -1;
            goto err0;
        }
        else {
#ifdef __SWITCHROOT_MAIN__
            PRINTF("ln -s %s %s\n", lpath, dest);
#else
            if (symlink(lpath, dest) < 0) {
                PRINTF("cannot create symlink `%s'\n", dest);
                result = -1;
                goto err0;
            }
#endif
            free(lpath);
        }
    }

  err0:
    free(dest);
    return result;
}


int switch_root(const char *new_root,
                MSGF_TYPE msgf, int msgdev)
{
    const char *initprogs[] = {"/sbin/init", "/bin/init", "/bin/sh", NULL};
    char *init = NULL;
    char *initargs[] = {NULL, NULL};
    char *src;
    int i;

    _msgf = msgf;
    _msgdev = msgdev;

    PRINTF("New root is `%s'.\n", new_root);

    /* copy etc */
    src = concat_subpath_file(new_root, "etc");
    PRINTF("Copying %s to /.\n", src);
    if (copy_file(src, "/etc", FILEUTILS_RECUR | FILEUTILS_FORCE)) {
        PRINTF("Copy failed.\n");
        free(src);
        return -1;
    }
    free(src);

    /* clean root */
    PRINTF("Cleaning old root.\n");
    if (filter_run("/", clean_root))
        return -1;

    /* bind new root */
    if (filter_run(new_root, bind_root))
        return -1;
    /* find init and run */
    for (i = 0; initprogs[i] != NULL; i++) {
        if (!access(initprogs[i], X_OK)) {
            init = strdup(initprogs[i]);
            break;
        }
    }
    if (init == NULL){
        PRINTF("init not found in new root.\n");
        return -1;
    }

    /* say 88 to old init */
    PRINTF("Run init of new root.");
    initargs[0] = init;
    execv(initargs[0], initargs);
    PRINTF("exec of init failed.\n");
    return -1;
}

#ifdef __SWITCHROOT_MAIN__
int been_there_done_that = 0; /* Also used in applets.c */
const char *bb_applet_name;

int main(int argc, char *argv[]) {
    if (argc != 2) {
        PRINTF("Usage %s <new root dir>\n", argv[0]);
        return -1;
    }
    filter_run("/", clean_root);
    filter_run(argv[1], bind_root);
    return 0;
}
#endif
